【笔记】《编程原则:来自代码大师Max Kanat-Alexander的建议》读书笔记
Jul 28, 2024笔记编码软件工程《编程原则:来自代码大师Max Kanat-Alexander的建议》(Understanding Software)读书笔记
作者介绍:马克斯·卡纳特-亚历山大是谷歌的代码健康技术主管,他的工作包括担任Xbox上YouTube的技术主管,在谷歌从事Java JDK、JVM和Java其他方面的工作,以及担任YouTube的工程实践技术主管,他在YouTube上为所有的开发人员提供最佳实践和工程开发效率方面的支持。
1 | "关于如何对待编程领域中这些和编程间接、直接相关的知识,我见过两种极端的态度:有的人只看结果,只关心“写代码”,而对“写好代码”一无所知;第二类人深谙各种架构设计、整洁代码之道,但对于当下代码中遭遇的问题却没有落地的方案。 |
1 | Uncle Bob Martin在他的“The Principles of OOD”系列文章中谈到过糟糕设计(Bad Design)的几个特征: |
1 | "大部分时候——我说的是大部分时候,技术的决策是专制的。如果我在这个技术领域有丰富的经验,如果我解决过足够多的问题,哪怕是我在这个项目中待的足够久,那么对于当下任何一个新的问题,我就能想得更多,看得更远。当然如果团队的时间和人员充足,可以抱着培养新人的心态,放手把问题交给一个从没有接触过这方面领域的人来解决。" |
程序员应该了解的基本原则
第1章 在你开始之前
要成为一名杰出的程序员,你必须首先想要成为一名杰出的程序员。
要做就把它做好
第2章 工程师的态度
在每一类工程领域里,每一位工程师都应该有的工作态度是:我可以用正确的方式解决这个问题。
无论这个问题是什么,解决问题的正确方式总是存在的。它不仅触手可及,在项目中也存在落地可能性。唯一不这么去做的正当理由只可能是缺少资源。
“正确方式”通常指“在考虑到未来所有可能发生的合理情况的前提下给出的解决方案,这个前提甚至包括那些未知的和难以想象的情况”。
如果软件代码在保持简约的同时,也为将来可能出现的合理功能变更需求提供了灵活性,那么就可以说它是以“正确方式”设计的。
第3章 成为明星程序员的独特秘密
越是理解你正在做的事情,就越是能把它做好。“明星”程序员比一般或者平庸的程序员更透彻地理解了他们正在做的事情。仅此而已。
与相信自己“对一切了如指掌”相距甚远的是,许多程序员(包括我在内)常常感觉自己身处于浩瀚无垠的信息海洋里,受困在一场史诗级战争中。有太多东西需要知道,以至于哪怕穷极一生致力于学习研究,可能依然只了解了90%的计算机知识。
这场史诗级战争中的神秘武器,击败计算机知识的王者之剑,就是对你所学习到的知识的理解。
越是理解所处领域的底层知识,学习高级别的知识就越容易。越是理解当前级别的知识,学习下一个级别的知识就越容易,以此类推总是成立的。如果你自认为对某一门学科内从基础到高深的知识要点都统统掌握了,那不妨选择从头开始温习一遍,相信你会惊奇地发现在底层还有如此多的东西需要学习。
成为杰出程序员的必经之路就是保证对知识完全和完整的理解,从对基础知识的深刻掌握,到对大多数先进概念的扎实了解都必不可少。
第4章 两句话总结软件设计原则
软件设计的主要原则可以浓缩为两句话:
- 1.减少维护成本比减少实现成本更重要。
- 2.系统的维护成本与系统的复杂度正相关。
这大概就是设计原则的全部了。
软件的复杂性和它的起因
第5章 复杂性的蛛丝马迹
你可以利用以下特征来辨别代码是否过于复杂了:
- 1.需要添加“hack代码”来保证功能的正常运行。
- 2.总是有其他开发者询问代码的某部分是如何工作的。
- 3.总是有其他开发者因为误用了你的代码而导致出现bug。
- 4.即使是有经验的开发者也无法立即读懂某行代码。
- 5.你害怕修改这一部分代码。
- 6.管理层认真考虑雇用一个以上的开发人员来处理一个类或文件。
- 7.很难搞清楚应该如何增加新功能。
- 8.如何在这部分代码中实现某些东西常常会引起开发者之间的争论。
- 9.人们常常对这部分代码做完全没有必要的修改,这通常在代码评审时,或者在变更被合并进入主干分支后才被发现。
第6章 创造复杂性的方法之一:违反你承诺过的API约定
API是某种形式的承诺:“你可以放心地完全按照我们描述的方式和我们的程序进行交互。”可一旦你的产品发布了新版本,并且在新版本中不再支持旧版本API,那就意味着你违反了这种承诺。它给软件增添了复杂性。
曾几何时你的API用户只需要调用一个简单函数就能完成工作,而现在他们需要对你的应用进行版本检测,并依据检测结果调用两个不同函数中的其中一个。为了同时兼顾新版本函数,他们必须采用和之前完全不同的方式来向函数传递参数,导致代码的复杂性被无辜地加倍了。如果你改变的函数数量过多,为了适应全新API的工作方式他们可能需要将整个应用重写!
如果你频繁地打破API约定,那么他们的代码为了适配也只能变得越来越复杂。唯一的额外选项就是让他们的产品不再与你的旧版本产品兼容。因为对于用户和系统管理员来说始终设法保证两者之间的同步是一项极其困难的工作。即便对于你个人来说,维护旧API也是痛苦的,摆脱它能够使工作轻松不少。
避免这个问题的最佳方案是不要发布糟糕的API。或者(从用户的角度上看)更恰当的是,在承诺会始终维护旧版API的同时,以其他方式提供可被访问的全新API。
举个例子,如果你想要访问salesforce.com某些旧版本的API,只需要在和程序交互时使用不同的URL即可。而每一次在和Salesforce API进行交互时,URL事实上都为你间接地明确指定了你希望使用的API的版本是什么。
无论在什么情况下,对外发布一组极不稳定或是设计拙劣的API,要么会让你的工作变得复杂(因为你需要永远保证向后兼容),要么会让你API用户的工作变得复杂(因为他们为了能同时兼顾“好”版本和“坏”版本的API而不得不修改所有的应用)。
如果你选择违背API约定并且决定不再向后兼容,请别忘了当中的一些API用户永远不会为了适配新的API而更新他们的产品。或许他们只是没有足够的时间和资源来更新他们的代码。或许他们在使用第三方工具来和你的产品进行交互,但是第三方工具的维护者已经不再提供更新了。无论是哪种情况,如果他们修复代码的成本高于适配你的新产品而带来的收益,他们就会依然选择使用你的旧版本产品,甚至永远用下去。
所以在研发资源充裕的情况下话还是应该对外提供一组可供访问的API。但是在实现之前请务必对API进行精心设计。你可以在正式发布之前自己多尝试使用看看。还可以细心地对你的用户进行调研并且发掘他们究竟会如何使用你的API。总的来说,在发布之前就需要尽你的全部所能来保证API的稳定。在未来你需要投入多少年精力来维护API并不重要,重要的是在发布之前采取一些明智的手段来了解API在现实场景里应该如何工作。
API一旦发布成功,如果条件允许的话,拜托请千万不要违背你的API约定。
第7章 什么时候不值得向后兼容
因为向后兼容而引发问题的最好例子就是Perl编程语言。
当不计其数的人都在这么使用,并且对他们来说改变习惯非常困难的话,结果就是很大程度就不得不保证向后兼容。但如果维持向后兼容这件事确实阻碍了技术向前发展,那么你就需要警告人们这些“老掉牙的玩意”应该消失,并且是时候对它们说再见了。
你的另一个选择是无节制的向后兼容并且不再向前发展,这意味着对你的产品判了死刑。
这很好地说明了为什么你不应该漫无目的地给你的程序添加功能。因为总有一天你需要为这些你开发的“尽管没有什么用但加上去很方便”的功能提供向后兼容的支持。这是在添加新功能时需要慎重考虑的一点——既然这个特性已经存在于你的系统中了,那么你打算永远把它维护下去吗?答案是:你很可能需要。
理想的解决方案是:如果你不想在许许多多的后续版本中支持这些功能,那么当下就不要添加它们。
有时候需要丰富的编程经验才能有效地做出这样的决策,但你可以从这个功能的角度思考:“它真的这么实用吗,以至于值得我在未来的三到四年里在它上面花费至少10小时的开发时间?”这种用于评估应该花费多少精力在某件事物上的方法适用于万事万物,包括向后兼容、质量保证,甚至对评审极小的功能也同样成立。
一旦你拥有了一个功能,就意味着维护它的向后兼容性将会是日后的绝大部分工作。
你应该认真考虑放弃向后兼容的唯一时机是,当它在妨碍你添加明显实用且重要的新功能的时候。如果这种情况确实发生了,那么你就需要放弃向后兼容了。
第8章 复杂是牢笼
如果你编写的代码如此复杂以至于没有人能理解它怎么办?好吧,结果就是你个人会被永远地束缚在这个项目上面。
复杂是牢笼,简单是自由。
简约与软件设计
第9章 设计要从头抓起
你需要从一开始就着手于软件设计,应该从立项之初就致力于将架构设计得简约明了。
除非架构设计支持轻松地将该功能实现,否则我们绝不允许新增该功能。
如果你不考虑未来,那么你的所有代码都会陷入糟糕的设计和极度的复杂之中。“我们等不及了!这个功能非常重要!”又或者是:“现在只管把它加进来就好了,完事之后我们会把代码整理重构的!”他们从没意识到他们的态度一向如此。等到下一次需要添加另一个功能有求于我们时,他们还会说出同样的话。
如果只是新增很少的功能,并稍后将它重构的话就不太有可能出现这种问题。但如果空降一个架构无法支撑的大型功能,还计划在完成之后尝试整理代码,那这将会是一项艰难的任务。所以说功能的体量很重要。
最糟糕的情况是,在你允许人们在几个月或几年内不经过提前设计就往代码中新增功能,然后有一天你幡然醒悟并意识到系统有可能撑不住了。此时你唯一的选择只能是修复整个代码库。这注定会是一项艰巨的任务,因为就像新增功能一样,它无法一气呵成,除非你想要重写整个应用。
如果你想要开始以正确的方式行事,那么你必须以正确的方式立即行动起来。为了解决当下的问题,你必须将整个流程拆分为简单的步骤,并逐步对设计中存在的缺陷予以修复。这通常需要数月甚至数年的工作投入——简直就是浪费。因为你本应该从一开始就将架构设计好,这样的话这些问题从根本上就可以避免。你应该事先把目光放长远一些。
如果你的项目缺乏严格的架构设计,并且它的体量还一直在持续增长,那么终有一天超乎你想象的复杂性会让你束手无策。
这并不意味着你从一开始就需要设计能够满足未来所有需求的大型通用架构,并且现在就实现它。上述观点想表达的是,你需要在工作学习中应用本书和《简约之美》中讨论的那些软件设计原则,这样从一开始你就会拥有一个可理解的、简约的并且具有可维护性的系统。
第10章 预测未来的准确度
预测软件的未来如此困难。预测未来的准确度,会随着系统复杂性和预测点距今时间跨度的增加而降低。也就是说随着系统变得越来越复杂,你只能以有限的准确度预测短时间范围内的未来。反之随着系统变得越发简单,你越能以高准确度预测较远的未来。
保证系统架构足够简单,便于你轻松地将旧语法替换为新语法。注意这里对系统架构的要求不是“灵活”,也不是“通用”,而是简单到易于理解和修改。
在现实工作中,存在一种基于以上准则扩展之后的逻辑先后关系:
- 1.预测未来的难度会随着系统和被预测功能所处环境内,所有修改之处数量总和的增长而增加。(注意,环境带来的影响与它和系统的逻辑距离成反比。如果你的系统与汽车有关,那么对引擎的修改可能会给系统带来非常大的影响,但是对环境内某棵苹果树的修改带来的影响则微乎其微。)
- 2.系统需要经受的修改与系统的整体复杂性相关。
- 3.所以:预测变困难的速率会与被预测行为所属系统的复杂性成正比。
不要想当然地依据你认为将来会发生的事情做出设计决策。请记住所有这些即将发生的事情都存在发生的概率,无论预测多少次都存在出错的可能。
当我们只关注当下,关注我们已有的数据,关注我们现有的软件系统,相比预测我们的软件在未来何去何从,我们更容易做出正确的决定。大部分在软件设计中犯下的错误来自假设未来需要做些什么(或者完全不需要做些什么)。
当你发现随着时间的推移,软件的某些代码变得难以修改时,这条规则会带给你帮助。你永远无法完全避免代码被修改,但如果你的软件简化到傻瓜都能理解的地步,那么修改的可能性就会大大降低。虽然它可能依旧会在软件质量和实用性方面逐渐衰退(因为你没有即时追随环境的变化对它进行修改),但是它衰退的速率远比复杂的时候要慢。
编写简单的软件比编写复杂的软件花费的功夫更少。虽然有时需要加入额外的思考,但总体来说需要的时间和投入会更少。所以尽可能保证架构的合理简约,就是在为我们自己取得一场胜利、为我们的用户取得一场胜利、为未来取得一场胜利。
第11章 简约与严格
一个普适的原则是:你的应用程序对编码要求越是严格,就越易于编写。
1 | 举一个例子,想象一个应用程序只接受数字1和2作为输入,并且除此之外的任何其他形式的输入都被统统禁止。那么即使发生在输入时的小小变化,比如在“1”之前或者之后增加一个空格都会引起程序的报错。这样的程序在被称为非常“严格”的同时也极易编写。你需要做的仅仅是校验:“他们输入的究竟是1还是2?如果都不是,则报错。”然而在大多数情况下,如此严格的程序显得不切实际。如果用户不了解你期望他们输入的格式,又或者如果他们在输入数字时不小心敲击了空格或者其他的字符按键,程序会拒绝“执行他们的意图”而给用户带来挫败感。 |
顺便说一句,如果你正在为程序员编写框架或者是编程语言,你的最佳选项应该是让用户接口“不那么严格”,甚至是尽可能地简约,这样就不必在可用性和复杂性之间权衡了,让开发者同时感受到两个世界的美好。
严格这个词大部分时候意味着你给用户的输入设置了一份白名单。在有些应用中,你还可以对输出做出严格的限制:输出通常需要迎合一类特殊并具体的标准。但是通常来说,你能接收什么样的输入以及什么样的输入会引发错误,这两件事会显得更重要。
或许最知名的与严格有关的灾难就是HTML。正因为它从一开就被设计成不那么严格,在经过几年的普及之后,导致处理它的兼容性问题成为浏览器设计人员的噩梦。当然它最终还是被标准化了,但那个时期的大部分的HTML代码阅读起来依然会令人抓狂,现在这种现象还是存在。因为它从一开始就不够严格,所以现在没有人敢打破向后兼容并将它变得严格。
总而言之,我坚持认为计算机永远不应该“猜测”或者说“尽全力满足”用户的输入。由此引入的噩梦般的复杂性会导致程序极易失控。猜测唯一能恰如其分发挥功效的地方是内置于类似于谷歌网站的拼写建议功能中。它提供你做事情的选项,但不会一股脑地基于猜测的结果去完成工作。这也是我在谈论严格时想强调的另一个方面,输入要么是对要么是错,不存在“也许”这种情况。如果一个输入有可能包含多层含义,要么你应该为用户提供选项,要么直接报错。
在计算机世界中人们从一开始就应该对很多事物做出严格的限制,正是因为这类约束的缺失,导致这些事物现在看上去复杂得有些可笑。
当然,对可用性的关注依然重要。毕竟,电脑是帮助人类完成工作的。但是你没有必要为了可用而兼容普天之下的所有可能发生的输入。那会导致你陷入复杂性的迷宫之中,如果你义无反顾地打算继续这么做,祝你早日找到迷宫的出口。可你要知道他们从来不会严格按照标准化的方式制作迷宫的地图。
第12章 两遍已太多
代码只在必要时才需要通用。
一旦我意识到自己正打算将同一份功能实现两遍时,就会开始执行这个步骤。
该原则中至关重要的一点是立即采取行动。我不允许代码中存在两种相互竞争的实现。我当下就将它们合并成了一个通用解决方案。另一个重点是我不会把它抽象得过于通用。
基于“两遍已太多”原则我们能进一步推导出:理想情况下,开发者修改某处代码的方式不应该与修改另一处代码的方式近似甚至相同。
这也就是说,开发者不应该在修改B类时必须“记得”去修改A类。他们也没有必要知道如果常量X发生了变化,Y文件也需要更新。换句话说,不仅两种实现会带来糟糕的开发体验,两个文件位置也会。虽然系统内的重复代码并非总能被合并且共享,但这应该是我们解决问题的方向。
当然,“两遍已太多”中最浅显的含义实属那条经典原则:“DRY”。所以不要用两个常量表示同一件事情,不要定义两个函数来干同一件事情,等等。
这条规则在其他方面也同样适用。总而言之思路是,当你发现对于单个概念存在两套实现方案时,你应该想办法将它们合并为单个解决方案。
在重构代码时,这条原则能够帮助你找到代码中值得改善的地方,并且能给予你一些重构方向的提示。例如在你发现系统中存在逻辑重复的地方时,你应该尝试将他们合并在一起。当另一处重复逻辑再次出现时,继续将该处合并到刚刚的通用逻辑中,如此重复执行。
也就是说如果有太多的代码需要进行合并,你可以按照对每两处执行一次合并的方式进行增量重构。采取什么样的方式并不重要,只要合并的工作确实能够让系统变得简单就好(易于理解和维护)。有时候你需要判断以什么样的顺序将这些代码合并是最有效的,但是如果你无法判断出来也不用担心——直接对每两处执行一次合并就好了,船到桥头自然直,通常重构的所有问题最后都会迎刃而解。
千万不要将不应该被合并的逻辑放在一起。将两种不同的实现合并在一起常常会给系统创造更多的复杂性,或者导致代码违反了单一职责原则,这条原则告诉我们:任意给定的模块、类或者函数在系统中应该只表示单一的概念。
举个例子,如果你系统中用于代表车和人的代码有轻微的相似之处,请不要通过把他们合并为“车人”类来解决这个“问题”。这样并不会降低复杂性,因为车和人的确是两类不同的事物,并且应该由两个独立的类来表示。
第13章 健壮的软件设计
我们做的最重要的一个决定,是确保整个过程足够简单。为了达成这个目标,我们让所有孔的尺寸都规范化起来,让所有操作都很简单且易于拆解。
调试代码
第14章 什么是bug
bug的精确定义:
- 1.程序的行为并没有符合程序员的预期。
- 2.程序员的预期没有满足绝大部分理性用户的期望。
通常来说只要程序能够严格执行程序员给出的指令,它就可以算是处于正常工作的状态。但有时候程序员期望程序执行的行为会出乎普通用户的意料,甚至给他们带来麻烦,所以这也算是一类bug。
其他软件功能上的不足都可以归纳到新功能需求中。如果说程序的工作状态的确与我们期望的一致,但离用户期望还有差距,则意味着它需要新“功能”。“功能”和“bug”定义之间的区别也就在这。
本质上说,任何导致程序员指令没有被正确执行的故障,都可以被认为是bug,除非程序员打算让计算机做一些它本不应该去做的事情。
第15章 bug的源头
bug通常来自开发者尝试降低代码复杂性未果而产生的副作用。也有部分来自对其实简单的代码产生的误解。
复杂的事物容易引起用户的误操作。在编程中也存在类似的情况,如果你无法轻易理解编程语言的文档,或者是这门语言本身,你就或多或少存在错误使用它的可能。
你每引入一丝复杂性,开发者(这里的“开发者”甚至包括你自己)误用你的代码的概率就高一分。
一旦代码的意图和使用方法变得极不明确,就会让使用这份代码的人犯错。又因为你的代码和其他的代码混合在了一起,导致了开发者误用和犯错的可能性大大增加。而后这些代码又会继续和其他的代码混合,形成恶性循环。
硬件设计者将硬件制造得极为复杂的情况时常发生。所以它必须与复杂的汇编编程语言集成。而这又使得汇编语言和编译器同样复杂起来。当你遇到这种情况时,如果你不提前对程序进行精妙的设计或者全方位的测试的话,基本上无法避免bug的发生。只要你的设计不够完美,那么在运行的一瞬间,大量的bug就会涌现出来。
站在其他程序员的视角看这件事也很重要。毕竟有些事对你来说很简单,但是对其他人来说或许很复杂。
如果你想要感同身受地体验一下其他人看不懂你的代码的感受,你可以找一份你从没有使用过的类库的文档来阅读看看。
也可以找一些你从没有阅读过的代码来阅读。尝试理解整段程序而不是单行代码的含义,并且想象当你需要对它进行修改时应该从哪里入手。这些都是其他人阅读你代码时的体验。你大概注意到在阅读他人代码时,即使并不复杂的代码也足以让人产生挫败感。
现在我们考虑另一种程序员误解简单代码的情况。这也是需要额外小心的另一件事。如果你察觉到某位程序员在向你解释一段代码时叙述得牛头不对马嘴,那便意味着他应该是误解了代码中的某些内容。当然如果他正在研究的领域极其复杂,也情有可原。
这两个方面是紧密关联的。当你编写代码时,需要承担的部分职责是让将来阅读你代码的程序员理解它,并且是很轻松地就能理解。如果你确实是这么做的,但是他在阅读过程中仍然产生了严重误解——或许他根本就不明白“if”语句是什么含义。那应该就与你无关了。
所以最后可以总结出几条有趣的原则:
- 1.你写的代码越简单,bug就越少。
- 2.你应该始终想方设法去简化程序中的代码。
第16章 确保它不会再发生
当你在解决代码中的问题时,你不应该止步于只修复问题表象。而是应该确保问题彻底消失并且永远不会再发生。
请记住,我们最在意的是软件的未来。软件公司代码库之所以会陷入无法维护的失控局面,是因为他们并没有真的在解决问题,只有切实解决这些问题之后,代码的可维护性才可能好转。
这也解释了为什么有的组织内部的紊乱代码始终无法回归到一个良好的可维护状态。当他们遇到一个问题时,他们应对问题的出发点仅仅是设法让提出问题的人停止抱怨,用这种态度解决问题之后继续以同样的态度应付下一个问题。他们不会考虑引入一个框架来阻止问题的再次发生。他们也不会追溯问题发生的根本原因然后斩草除根。所以他们的代码从来没有真正地“健康”过。
衡量一个问题是否被真的解决的恰当标准是:直到人们不需要再次对它进行关注。
绝对地做到这一点是不可能的,因为你无法预测到所有的可能性,但这条原则更多的是想提供理论上的指引而不是实际的操作指南。在大部分实际情况中,你能做到的是当下不会再有人被这个问题困扰,但是并不代表问题在未来不会再次出现。
可以提出更多的问题:
为什么开发者会写出错误代码?bug为什么会存在?是开发者接受的技能培训出了什么问题?还是工作的流程中存在纰漏?他们在编写代码的同时是否也应该编写测试?会不会是系统的设计缺陷导致代码难以修改?编程语言过于复杂了?他们用的类库编写的不够友好?操作系统出了什么问题?文档描述得不够清楚?
如果你有了关于某个问题的答案,你可以继续思考产生这个问题的根本原因又是什么,并且持续追问下去直到你所有的诱惑都已经解开。但是请小心:你并不知道这一串问题的终点在哪里,甚至整个过程会颠覆你对软件开发的看法。事实上从理论上来说,在这一套方法论下可以提出无限多的问题,并且终将让整个软件行业的根本问题得到解决。但是在这条路上要走多远还是取决于你自己。
第17章 调试代码的基本哲学
有时候人们在调试代码时会感受到强烈的挫败感。因为绝大部分人在调试系统代码时,倾向于将时间花费在思索而不是追溯代码的调用上。
当你开始调试代码时,请意识到其实你对答案一无所知。
人们倾向于相信冥冥中自己已经悟到了问题的答案。有时你确实能够猜对。这种情况不常发生,但是发生的频率之多,让不少人误以为猜测也是调试代码中的有效手段之一。
大部分时候,你可能会花上几个小时、几天甚至几周来猜测问题究竟出在哪里,并且尝试各种除了让代码更复杂之外毫无实际用处的修复方案。你会发现在一些代码库中充斥着仅依据猜测编写的用于修复“bug”的“解决方案”——这些所谓的“解决方案”恰恰是代码库复杂性的一大来源。
通常来说,成功对bug进行修复,也应该意味着系统在变得更好,比如系统变得更简单了,架构设计得到了优化,等等。
通常,bug的最佳修复方案,会在修复问题的同时,间接地移除冗余代码,并且简化系统设计。
基本上在遇到问题的第一时间内,你脑海中冒出的想法都属于无稽之谈。此时此刻你需要了解的只有两件事:
- 1.记住系统正确的行为是什么。
- 2.想清楚应该通过追踪哪一部分代码来收集更多的有效信息。
这才是调试代码中最重要的原则:调试代码指的是在你找到问题的起因之前,持续收集信息的过程。
可以通过深入了解系统的工作原理来收集信息。以服务器无法返回页面的情况为例,或许你可以在通过查阅系统日志找到线索。又或者你可以尝试重现问题,并通过观察服务器此时的工作状态来发现蛛丝马迹。这也是为什么处理问题的人总是希望能“还原现场”(通过一系列步骤能够让你复现问题)。这样他们就能在bug发生时回溯出了什么样的问题。
有时你的首要任务是明确bug究竟是什么。通常用户上报的bug信息内容都相当有限。你的用户越是没有计算机相关背景,在缺乏引导的情况下他能够准确表达问题的可能性就越低。在这些情况下,除非问题十分紧急,否则我首先要做的事情就是请求用户给出更详细的出错信息,并且在我得到回复之前我不会采取任何行动。也就是说,在他们明确bug之前我绝不会自行尝试解决这个问题。
如果在对问题一知半解的情况下就着手尝试解决它,那么我可能会把时间都浪费在查看各种和问题无关的系统随机角落上。所以为了让时间花得更有价值我才选择等待用户的进一步反馈,并且最终当我确实拿到一份完整的bug报告时,我才会着手探寻bug背后的原因。
请注意,不要因为用户提交的bug信息不够丰富而迁怒于他们。虽然他们对系统的了解不如你,但并不意味着你有资格用不屑的态度鄙视他们。为了获取信息你应该直言不讳地提出问题。
要知道引导他们提供正确的信息也是你的工作职责之一。如果人们总是无法提供正确的信息,你可以尝试在报错页面提供一个表单来帮助他们梳理出正确的信息有哪些。我想表达的是帮助其实是互惠的,只有你帮助了他们,他们才能反过来帮助你,这样你才更容易地解决问题。
一旦明确bug,接下来你就需要对系统的不同组件进行排查以找到错误原因。至于从哪些组件入手排查取决于你对系统的了解程度。通常是从日志信息、系统监控、错误消息、核心转储或者是系统其他的输出信息入手。如果系统无法为你提供这些信息,你或许需要考虑在继续排查问题之前,发布一个能够收集这些信息的新版本系统。
尽管对于只修复单个bug而言,这看上去似乎需要耗费不少的工作量,但相比你在系统内毫无目的地碰运气来猜测问题的原因,发布能够提供有效信息的新版本系统还是能够提升不少效率的。这也是支撑快速发布、频繁发布实践的有力论点:发布新版本的频率越高,你收集到的调试信息速度也就越快。有时你甚至可以定向地为遇到问题的用户发布新版本系统,这也可以作为收集信息的捷径。
调试代码是一类将已有数据与期望数据进行比较的行为。
当你意识到找到问题的根本原因时,是当你十分肯定在将它修复完毕之后错误就再也不会发生了的时候。这不是绝对的——关于如何“修复”bug还有可以讨论的空间。bug需要修复到何种程度取决于你的解决方案想解决到哪个层次,以及你想要在上面花费多少时间。通常在你找到某个问题的深层原因,并且将它修复之后,就能看出你最终做出了什么样的选择——这再明显不过了。但我依然想要警告你,只解决问题的表面症状而不解决引起问题的深层原因是有风险的。当然,在找到原因的当下就马上修复它。这其实是正常情况下最直接的方式。
调试代码的四个主要步骤:
- 1.熟悉正常工作的系统行为应该是什么样的。
- 2.接受其实你并不知道问题原因的这个事实。
- 3.追踪代码直到你找到问题的原因是什么。
- 4.修复根本原因而不是表面症状。
1 | 这听起来十分简单,但我基本上看不到有人能遵守这一系列准则。我的所见所闻是,大部分程序员在遇到bug时,喜欢坐下来思考,或者通过询问他人找到问题可能发生的原因——这两种做法都无异于猜测。 |
有时候“收集信息”的过程会相对困难,特别是对于那些你无法重现的bug,但最坏的情况也无非是通过阅读代码来收集信息,尝试找到代码中bug所在,又或者把系统的工作流程图画出来,看是否能发现症结在哪里。我建议把这些方法当作没有办法的办法,但是即使你这么做,也比猜测问题出在哪里或者假设你已经知道问题在哪里要强。
团队里的工程问题
第18章 高效工程开发
通常来说,致力于改善团队开发效率的同事会陷入两难的局面,要么他们会和他们所服务的开发者产生冲突,要么他们的时间都花费在一些截止时间遥遥无期的项目上面,因为大家对这些项目漠不关心。之所以会发生这种情况,是因为开发团队认为有待解决的问题并非实际存在的问题。
随着时间的推移,负责效率改善的人员会对周围合作的同事形成一种敌对的态度。他们认为如果其他的工程师如果能够“使用我开发的工具”,那么所有麻烦都能迎刃而解。但是开发者最终并没有选择使用你编写的工具,所以你又凭什么认为你的工具举足轻重呢。
问题在于,一旦你开始忽略其他开发者的抱怨(又或者完全意识不到他们遇到的问题),你们之间对立的种子就已经种下了。它不是一个由好变坏逐渐腐化的过程。而是从一开始当你认为问题是这个,而其他开发者认为问题是那个的时候,矛盾就诞生了。
如果你做了一大堆的重构工作,但是根本没有人继续维持重构后代码的简约,又或者你写了一堆没有人使用的工具/框架,那本质上你还是和什么都没有做一样,太令人沮丧了。
当你在解决开发效率低下的问题时,开发者是你解决方案的用户。你不能无脑地同意其他开发者提供给你的关于如何实施解决方案的建议。这么做可以在一定程度上哄这些人开心,但这终将会让系统变得难以维护,而且也仅仅是满足了那些叫喊声最大的用户的需求而已——他们很可能并不代表你的大部分用户。如果你接受了他们的建议,那么你最终会得到一个设计混乱,甚至连真实用户需求也无法满足的系统。
举一个例子,假如开发者向你抱怨他们某个千万行代码的单体二进制代码发布流程太慢了,接着你就把时间都花费在想方设法让发布工具变得更快的工作上,结局是多半你不太可能带来好的改善。或多或少能带来一些改善(让发布更快),但是永远也没有解决根本问题,根本原因是这个二进制代码体积太大了。
你要做的第一件事是明确开发者认为的问题所在。不要做任何的评判。四处走走和不同的人聊聊。多听听那些直接和代码库打交道的人的意见。如果你没有机会和每一位工程师交谈,可以先从与每个团队的技术管理者沟通开始。然后你可以和管理层聊聊,毕竟他们也有你需要予以记录和解决的问题,你需要对这些问题进行了解。但是如果你只想要解决开发者遇到的问题,你应该从开发者身上找出问题是什么。
一般来说,如果你直接问开发者代码的复杂之处,他们不一定能回答上来。如果你问“什么地方过于复杂了”又或者“你认为的难点是哪”,他们可能想了半天也给不了你答案。但如果你希望得到大多数开发者对于他们编写或使用的代码的情绪上的反馈,那么他们还是有很多话可以说的。我会问一些类似于这样的问题,“这份工作有什么让你感到闹心的地方吗”,“哪一部分代码你修改起来最不爽”,“代码库中有什么地方是你因为害怕改坏了而不敢修改的”。如果面对经理我会问:“代码库中有没有开发者常常抱怨的地方?”
你可以根据你的情况对这些问题进行调整,但请切记你是真心想要和开发者们进行一次对话——而不是机械地把问题列表读一遍而已。他们会说一些你有兴趣深入了解的事情。你可能需要把当中的一些内容记录下来。
在这项工作持续一段时间后,你大概就能察觉到这些抱怨中的共通点(或者某些共通点)。这并不是我们想要寻找的唯一原因——即使没有和大家交谈我们大概也能猜出来。我们想要寻找一些更高层次的原因,类似于“构建二进制文件过于缓慢”。有更多类似的原因有待我们发掘。
首先你可以从收集的信息中找到那些开发者已知的,且能在短时间(比如一至两个月)改善的问题,并给出解决方案。解决方案没有必要完全颠覆现有工程师的开发模式。事实上它也不应该这么做。因为当前变革的重点是为了建立大家对你工作的信任。
提升开发效率的成功与否,取决于你的个人信誉。
你可以预见总有一天你需要解决本质上的问题。只有当其他开发者对你有足够的信任,你才有机会朝那个方向努力,当你想要做出一些改变时,大家会相信你的解决方案是正确的。所以你首先需要做的事情是,在团队中树立自己的可靠形象。
通过解决第一个问题,大家已经对你有了基本的信任,接下来你可以着手搜寻开发者真正面临的问题,以及最佳的解决方案可能是什么。这通常不可能一气呵成。此时你需要注意到另一个知识点——你不能一下子推翻并重建所有的团队文化和开发流程。你必须以渐进的方式,将变革逐步“渗透”到大家的工作中(人们通常会因为你改变了什么,或者改得面目全非,又或者第一轮变革并不起作用而感到生气),等到大家适应之后再考虑推进下一步工作。
如果你试图将变革一步到位的在团队内推广生效,一定会有人公开的反对你——这些反对的声音会让你的个人信誉荡然无存,还会使得你所有的努力付之东流。于是你又不得不回到之前提到的两个毫无建设性的解决方案——要么团队变得士气低下,要么毫无起色。所以你必须按部就班地展开工作。有的团队可以接受较大程度的变革,有的只能接受较小程度的变革。通常团队的规模越大,你执行的过程越要缓慢。
你应该找一批支持你的人,建立一个能为你付出的努力背书的核心圈子。绝大部分程序员还是帮理不帮亲的,即使他们口头上什么也没有说。
当有人提出他们的长远改善计划时,你应该公开鼓励他们。不要要求每个人都做出完美的改变——你的当务之急是凝聚你的“团队”来验证清理代码、效率提升的种种手段是有价值的。你还要负责营造志愿者文化或者经营开源项目——你必须非常地有感染力和友好才有助于这些工作的推进。但这并不意味着你应该接受糟糕的改变,但是如果有人想要做出改善,你应该至少对他们表示肯定和赞许。
有时十个人里有九个人想要做正确的事情,但他们的声音会被那个嗓门最大的人的声音所掩盖,以至于他们想当然地认为应该尊重那一个人的想法,而不是据理力争。所以你应该尽力争取这一部分人的支持,这有助于你工作的展开。通常,忽略那个嗓门最大的人的声音继续一往无前地改善工作也是办法之一。
如果你终究还是被某些高层人士一致叫停,可能存在两种情况:
(a)解决问题的方式有所偏差(可能是你并没有按照我上面推荐的方式去执行,也可能是在和团队的沟通上出现了问题,还有可能是你正在做的事情会对开发者造成负面影响等)。(b)叫停你工作的人愚蠢至极,无论他们看上去多么地“正常”。
如果你的工作被叫停是因为你正在做错误的事情,那么找出什么对开发人员最有帮助,然后回归到正确方向去做就好了。有时这只需要和那位叫停你的高层人士好好沟通就能找到答案。
假设你现在正在通过渐进的方式,有条不紊地改善团队的开发效率,一些潜在障碍也逐渐被清除。那么接下来该何去何从?答案是请确保你的前进方向瞄准的是本质问题。
总有一天你需要解决这个问题,而解决的方式之一是需要纠正人们编写软件的方式。
先不要对外发布承诺,不要大声宣布你有一揽子改善开发效率的计划,并且计划是从重构代码开始的。
你应该希望人们产生一种思维惯性,比如“开发也意味着对代码进行整理”或者是“代码的质量也很重要”。也可以是其他你希望营造的文化氛围。
一旦你在团队内成功建立起了一种改善代码的团队文化,即使你不再对它进行过问,问题也会随着时间推移迎刃而解。这并非意味着工作就此结束了,一旦每个人都关心代码质量、测试和开发效率时,你会发现即使没有你的积极干预,事情也能开始向好的一方面发展,但最坏的情况也不过如此。
请牢记,整个流程的重点并不是在于“达成共识”。你并非在争取团队中每个人关于你应该如何完成你的工作的许可。而是在找到人们认为的问题所在,并且提供一个解决方案将其修复完毕,这个他们认可的解决方案不仅能够建立起大家对你的信任,还能逐步解决代码库的深层问题,并且确保它并不是为了迎合某个人而诞生的。你只需要记住一件事:解决那些人们认为他们面临的问题,而不是你认为他们面临的问题。
最后一件我想要说明的是,所有这些技巧的前提是,你作为个体独自在负责整个公司或者整个团队的效率提升。还存在一些其他的场景——事实上,这并不是大部分效率提升工作的常态。实际工作中有的人会负责一部分工具的研发、有人负责框架的研发、有人负责和下属团队打交道等。
第19章 量化开发效率
一般我会优先把工作重点放在简化代码的设计上,我认为量化每一位开发者干的每一件事并不重要。几乎所有的软件问题,都是因为没有成功采用软件工程中的原则和实践。所以即使缺乏衡量标准,如果你能设法让整个公司都采用同一套好的软件工程实践,大部分的效率瓶颈和开发中遇到的问题都会自动消失。
有一种说法是,如果能将一切量化的话,这终将能带来巨大的价值。它能帮你识别出编码难点,允许你奖励那些效率提升的员工,允许你在效率欠佳的部门花更多的时间展开效能提升工作,当然还有其他数不清的好处。
但是编程不像其他的职业。你没法像量化制造业流程那样对它进行量化,在制造业中你只需要统计从流水线上检验合格下线的产品数量。但是你如何衡量一个程序员的产出呢?
秘诀在于对“效率”进行恰当的定义。理解效率的关键在于,意识到它与产出物有关。一个有效率的人通常都能够高效地输出产出物。
衡量开发者效率的方式之一是衡量他的产出物。
如果你想衡量一个人的产出物,你不应该去判断他掌握这门手艺的精湛程度。你应该衡量通过这门手艺他带来了多少产出物。
第一件需要想明白的事情是:对于用户来说,程序的哪一部分产出是最有价值的?软件的目的其实是“帮助其他人”。所以第一步就是确定哪一类人群是你的软件帮助的对象,以及在使用产出物为他们提供帮助时,会带来何种影响。
例如你负责研发和维护一款用于个人用户报税的会计软件,你可以把通过使用你的软件,成功且准确地填写了纳税申报的人数作为有效指标。当然软件的成功离不开公司内每一个人的努力(包括销售人员在内),但是程序员需要为软件的易用性和质量属性负主要责任。
有的人喜欢挑选那些程序员全权负责的事物作为指标,我建议不要盲目地依赖它——如果想要将它作为衡量个人产出物的有效手段,程序员不一定是唯一能够对它产生影响的人。
量化一个系统的指标也是多种多样的。假设你为一个购物网站工作。后端开发者或许会以成功执行的数据请求数量作为某项指标,而前端开发者则以成功添加进购物车的商品数量,以及成功通过结算流程的人数作为某项指标。
当然,单个候选指标也应该与整个系统的指标对齐。如果后端开发者只是衡量“后端接收到的请求个数”,而不考虑成功执行的情况,也不考虑执行的响应速度,那么他们完全可以设计一个需要反复调用多次的糟糕API,这无疑对用户体验造成了伤害。
所以你需要确保心目中的候选指标,是和帮助现实用户息息相关的。对于刚刚的例子,一个更好的解决方案可以是,多少次“提交支付”的请求被成功处理了,因为这才是最终结果。(顺便说一声我不会将此作为购物网站后端的唯一可能指标——它只是一个可能性而已。)
但无论你衡量的标准是什么,重点在于即使我们衡量的部分人员的产出物是代码,你衡量的依然是产出物。
还存在最后一种情况,就是如果他们的职责是负责改善开发效率。如果你的工作内容是帮助其他开发者提升对于需求的响应速度,你要怎么量化你的工作成果?
首先,大部分负责改善开发效率的人员都有属于他们自己特别的产出物。产出物可能是一个测试框架(也就是说你可以用上面所说的衡量一个库的标准衡量它),又或者是其他某些开发者可能会使用的工具,在这种情况下你可以统计工具的使用情况或者人们对它的满意度。
举个例子,bug管理系统的开发者们想要量化的指标之一,是bug被成功和迅速解决的个数。当然,考虑到工具在公司内部是被使用的方式,指标还需要稍做修正——或许有一些系统中的bug记录压根就不需要被快速修复,甚至将会长时间存在,所以你要想办法用其他的方式衡量它们。总的来说,你应该问自己:我们使用的这件工具,带来的产出物和造成的影响究竟是什么?这才是你应该衡量的——产出物。
但如果你并不是在开发一些具体的框架或者工具怎么办?有可能你的产出物和软件工程师这个群体息息相关。此时或许你可以衡量你的工作成果给工程师带来帮助的次数。或者统计你引入的改善给研发工作节省下来的时间,当然前提是你能准确地进行统计(基本是不太可能的)。总而言之,与量化其他类型的编程工作相比,量化这些工作会更加困难。
如果某人负责改善特定团队的开发效率,那么应该衡量团队体验到的效率提升程度。又或者衡量团队指标的提升速率。
第20章 如何应对软件公司内代码的复杂性
只有依靠程序员个体才能解决代码的复杂性问题。也就是说想要解决代码的复杂性,需要每一个人都对代码保持警惕。他们当然可以借助一些工具来减轻这项任务的压力,但简化代码这份工作终究还是需要人们脑力、注意力和汗水上的投入。
解决代码的复杂性问题,离不开每一位个体贡献者的底层代码工作。
如果管理者只是在下达“简化代码!”的指令后就拍拍屁股一走了
之,通常什么都不会发生,因为:
- a.员工们需要更明确的指令;
- b.员工们对被需要改善的代码一无所知;
- c.对问题的理解其实是发生在解决问题的过程中的,管理者并不是解决问题的人。
如果你是一名软件工程经理,你可能会提出一类大而全的、能够一劳永逸解决所有问题的解决方案。通过这种方式解决代码复杂性的问题在于,代码问题通常在许多不同的子项目中,需要许许多多程序员个体落实到代码细节层面才能修复,这种一揽子的解决办法不切实际。
所以如果你想依靠一个大而全的解决方案来应对一切问题,你会发现它其实并不适用于所有场景。并且这么做只会适得其反,软件工程师们看上去做了很多工作,但实际上他们并没有产出一个具有可维护性且简单的代码库。
所以如果你作为一名管理者正在负责一个结构复杂的代码库,你需要做些什么来改善这些代码呢?解决问题的关键在于从每一位开发者身上获取信息,并与他们一同工作,从而帮助他们解决问题。
第一步——列出问题:
询问团队里的每一位成员,邀请他们把代码中最让他们受挫的地方写下来。代码复杂性引起的现象,会导致人们对于代码产生本能的情绪性反应,例如对代码感到疑惑,感觉到代码是极易损坏的,认为代码难以优化等。所以你可以提出类似这样的问题:“系统里有什么地方的代码是在你修改时会感到紧张的?”或者是“代码中有什么你曾经维护过的地方让你感到非常棘手?”
每一位软件工程师都应该把他们心目中关于这些问题的答案都写下来。我不推荐通过使用某个系统来收集这些信息——对于他们来说手写是最简单的方式。可以给他们几天的时间来整理答案列表,因为他们可能需要一些时间考虑。
这份列表可以不仅仅包括你负责的代码库,任何关于他们曾经维护过或者使用过的代码的吐槽都可以记录其中。现阶段你只是在收集症状,并非原因。对于这份回答来说,开发者们的表述可粗可细。
第二步——举行会议
召集你的团队举行一个会议,确保每个人都带来了他们关于那些问题的答案,以及能够访问代码库的电脑。团队会议理想的参与人员人数大致在六到七人左右,如果团队人数过多的话,你需要再将他们划分为更小的队伍来举行会议。
在会议上你应该挨个过一遍所有的回答,找到每一个症状对应的文件目录、文件、类、方法或者是代码块。
即使有人的回答是:“整个代码库都没有单元测试。”
你也应该刨根问底地问下去:“请告诉我这个问题会在什么时候对你造成影响?”
再根据他的回答找到现阶段最需要为之编写测试的文件是哪些。
你还需要确保你获得了关于问题的准确描述,类似于“重构代码非常困难,因为我不知道我的修改是否会破坏其他人的模块”。这种情况下单元测试似乎是一个解决方案,但是你首先还是需要尽可能地把问题的范围缩小。(的确所有代码都应该有对应的单元测试,但如果现在你的代码库中一个单元测试都没有,你需要从这方面的一些可行的任务开始。)
总而言之,只有代码才是能够被实实在在修复的,所以你需要知道哪一部分的代码出现了问题。当然还存在着影响面更广的问题有待我们解决,但是再大的问题也可以被拆解为更小的问题来各个击破。
第三步——bug报告
利用从会议中收集到的信息,为每一个问题(不是解决方案,只是问题!)生成一则bug报告,并且可以用这个问题关联的文件夹、文件以及类名作为bug报告的标题。例如“FrobberFactory类太难以理解了”。
如果在会议上问题的解决方案同时也有了结果,你可以在报告中进行备注,但是报告本身还是应该以问题为主。
第四步——决定优先级
现在是时候决定问题的优先级了。首先要找到哪一个问题影响到的开发者数量最多。这些都是高优先级的问题。通常这部分工作是交由团队或者公司内对开发者最了解的人来完成。一般是团队经理。
有时候需要考虑问题间的依赖关系而不仅仅是严重性。举个例子,解决问题Y的前提是解决问题X,或者是如果问题A提前得到解决的话,问题B解决起来会更容易。
这意味着问题A和问题X即使没有它们后续的问题看起来那么严重,它们也应该优先被解决。大多数时候都会存在这么一条问题链,关键在于找到链路源头的问题是什么。
没有正确评估问题的优先级,是软件设计中常犯的错误之一。虽然这个步骤看上去无关痛痒,但它对降低解决代码复杂性的成本至关重要。
无论何时何地,软件设计的精髓在于以正确的顺序做正确的事情。
强迫开发者以无序的方式解决问题(忽略问题间的依赖关系)会加剧代码的复杂性。
无论你是在开发前期还是开发过程中完成的这部分工作,非常重要的一点是确保让每一位程序员意识到,在他们开始分配正式任务之前,首先需要解决一些前置任务。他们必须取得足够的授权,能够从当前需要完成的任务,切换到优先解决那些阻碍他们的任务。
第五步——分配任务
现在你可以把每一个bug分配给不同的具体开发者。可以说这是一个相当标准化的管理层面的流程了,虽然它涉及具体的沟通和工作细节,但我相信大部分软件工程经理对此已经驾轻就熟。
有一个意外情况是,可能其中一些导致bug的代码并不是由你们团队维护的。这种情况下,你需要通过与组织层面进行沟通,找到负责解决这个问题的合适团队。如果你能从另一个与你有相同遭遇的经理那里得到支持是再好不过的了。
在一些组织内部,如果其他团队引入的问题并不复杂,也不需要了解过多的细节,那么你所属的团队就可以自行对它进行修复。这可以根据你们解决问题的效率和成本自行决定。
第六步——计划
现在你已经对所有bug进行了记录,接下来你必须要想清楚何时将它们修复。一般来说最佳的方式是确保开发者们会定期修复其中的一些问题,并且同时进行常规功能的开发。
如果你的团队通常以一个季度或者六个礼拜作为一个迭代周期,你应该在每个迭代周期内都安排一些代码清理工作。最好是让开发者们首先做一些能够让他们将来开发代码变得更轻松的代码清理工作,再开始正式的代码功能开发。
放心这通常不会拖慢开发进度(也就是说如果代码清理得当,开发者们依然能够在一个季度内把计划中的功能实现,这变相说明了实际开发时间减少了,同时开发效率得到了提升)。
不要为了代码质量而完全中止正常功能的开发。请确保提升代码质量的工作会持续进行下去,自然而然代码库总体上就会趋于变好而不是变差。
第21章 重构与业务功能有关
当你在清理代码时,你其实是在间接地为代码所属的产品提供服务。重构的本质是一类有组织的流程(这里说的“有组织”并不是指“与业务有关”,而是说“有序地将事物安排起来”)。也就是说,为了达成某个目标你在对事物进行有序的排列。
当你开始独自重构时,重构会给你带来一个坏的名声。人们会开始认为你在浪费时间,你在人们心目中的可靠程度会降低,你的经理和小伙伴们会设法阻止你接下来的重构工作。
我所说的“独自重构”意思其实是,你发现了一些与你当前工作不相关的代码,并宣布“我不喜欢它的架构设计”,然后在不影响系统功能的前提下对代码的不同部分做设计上的修改。
浇灌草坪的重点是你房屋前有一片不错的草坪。如果重构代码的部分和你当前负责的产品或者系统的实现目标没有任何关系,算下来你其实什么都没有做,只不过重构了一些没有人用或者没有人关心的代码而已。
通常来说,首先你需要挑选一个有兴趣上手的业务功能,然后找出通过重构哪一部分代码能够让你的开发工作变得更轻松。又或者找一些修改频率很高的代码,对它们进行组织优化。这会让人们对你的工作投来赞许的目光。人们赞许的背后有更深层的原因:事实上他们这么做是因为你目前的工作起到了事半功倍的效果。无论如何这至少算是一类对你工作成果友好的认可,并能够鼓励你持之以恒地坚持下去,也表示有人开始注意到你的工作,说不定还能和你一起把好的开发实践在公司里推广。
你是否可能需要重构一个与手头工作并不直接相关的项目代码?这是非常有可能的,有时候你需要重构一些与目标间接相关的代码。
清理复杂代码库的关键原则之一就是始终在特性服务中进行重构。
代码库实际处于这种状态——它变得更糟的速度比变好更快。你首要的目标,是想办法让系统变得越来越好,而不是越来越差。
你必须在达成业务目标与重构代码之间进行平衡。因为现实条件并不可能允许你一直将代码重构下去
一般来说,我会给需要修改的代码设定一个边界,例如“哪怕是为了实现业务目标,我也不会重构任何我当前项目以外的代码”或者是“我不会等到编程语言本身做出了修改之后才将这个功能发布”。
重构不是在浪费时间,而是在节省时间。总体工作时间只会更少或者持平而已。这里“总体”还包括了你花费在调试代码上的时间、回滚代码版本的时间、修复bug占用的时间、编写复杂系统运行测试的时间等。
当我在决定代码何时才算重构“完成”时,我的判断标准是当别人在阅读这段代码时,能清晰地辨别出我在代码中的设计模式,并且能够随着这个模式继续维护下去。
有时候我会编写一些文档用于描述系统的设计思路,确保人们能够按照这个方向维护下去,但我的理论是(这条真的就是个理论了——我还没有足够的证据证明它的正确性),如果我把代码设计得足够好,其实就用不着用于描述设计思路的文档。通过阅读代码,设计思路也许就能自然而然地呈现出来,当你需要添加新功能时,需要涉及的修改之处一眼就能找到,连犯错的机会都没有。但很显然,想要完美实现这个目标几乎是不可能的,但是软件设计中有一条普遍真相是:没有最好的设计,只有更好的设计。
这也是另一则用于判断你是否“本末倒置”,或者过度设计,又或者花费了太多时间设想应该如何重构这件事的标准——你是否在设法让它变得“完美”。它没有必要“完美”,因为根本就不存在“完美”。只有“出色地将它应该完成的工作完成”。在不理解代码开发目的的情况下,你无法准确判断代码设计的好坏。一种设计对一种目的奏效,另一种设计可能又对另一种目的奏效。
当你在重构代码时,你的出发点应该是将代码的设计修正为更符合它的当前用途。
第22章 善意和代码
软件工程根本就是一门人类学学科。
在多年对软件开发流程进行持续改善的过程中我犯下过许多错误,这些错误都有一个共同的特征,就是只把目光聚焦于系统的技术层面,而不考虑写代码的人类的因素。你会发现有人更关注性能优化而不是代码可读性;你也会发现某人从不写注释,却乐意把时间都花费在如何让脚本代码行数变得少上面;你还会发现有人不善于沟通,却对小型二进制类库崇拜得不行:这些都是人类因素引起各种问题症状。
软件与人相关,软件系统代码是由人编写的。同时也是供人阅读,由人修改的,无论理解与否也都与人有关。它们代表的是编写它们的开发者思想。代码是地球上最接近人类思想的一种产物。
在与一群软件工程师协同工作时,有一条非常重要的原则:用粗鲁的态度对待开发团队里的成员不会带来任何价值。
粗鲁地对待与你一同工作的同事不会带来任何的帮助。气愤地告诉他们某个地方做错了,或者做了不该做的事也无济于事。唯一行之有效的是,确保软件设计的各项准则被正确应用到了开发中,以及人们在遵循正确的方向让系统变得更容易阅读、理解和维护。但这一切都没有必要用一种粗鲁的方式来实现。有时你需要做的仅仅是告诉人们他们某个地方做错了就好了。你只需要实事求是地说出来——大可不必为了这件事蹬鼻子上脸地对他人进行人身攻击。
这不仅限于代码评审,每一位工程师都有他们想要表达的观点。无论你同意与否,你都应该倾听他们的想法。礼貌地接纳他们的表述。用建设性的方式与他们交流你的想法。
值得一提的是,有时候人们确实难免会生气。但是请相互理解。有时候你也会生气,当这种情况发生时你也希望你的同事能理解你,不是吗?
请给予他们犯错的空间。用友善的态度和他们一起工作,齐心把软件做得更好。
第23章 运营开源项目社区其实非常简单
想要维护好开源项目社区,以及让社区稳步地壮大,本质上来说取决于三件事:
- 1.让人们变得乐于贡献代码。
- 2.移除有碍于参与项目,以及贡献代码的种种障碍。
- 3.把贡献者留住,才能让他们持续贡献代码。
如果你首先能让人们对你的项目提起兴趣,然后让他们开始正式贡献代码,并且保证他们始终对项目不离不弃,那么你才算是成功组织起了一个开源社区。否则你并没有。
一旦某人开始参与项目贡献,有什么办法能让他一直贡献下去呢?我们如何留住贡献者?首先我们对所有过去离开了这个项目的人做了一个调查,询问他们为什么离开。这个调查允许他们自由发挥,允许人们填写他们想要回答的任何答案,然后我们制作了一份图表,用于展示整个项目十年来贡献者数量的变化,然后将图表的起伏与这么多年来我们采取的或者是没有采取的各种行动关联起来。
当一切完成之后,我给Bugzilla项目的全体开发者发送了一封邮件,邮件中详细描述这项研究的成果。如果你有兴趣的话你可以阅读整封邮件内容,但是我会在这里总结一些其中的发现。
1.不要让主干太长时间止步不前
传统的开源社区智慧认为,人们喜欢在添加新特性上,而不是在修复软件错误上工作。我不敢说它是绝对正确的,但是我想说,如果你只允许人们修复错误,那么他们中的大多数都不会耐着性子留下来。
我们解决这个问题的方式是不再冻结主干代码。取而代之我们会在之前“冻结”主干代码的时间点立即创建一个分支。并且主干也始终保持着开放的状态,用于接纳新功能的开发。
是的,正如你预料的那样,我们的注意力会被分散在主干和最新的分支上。当我们在提交修复代码时,需要同时提交到分支和主干上。在开发新功能的同时我们也要兼顾解决bug修复问题。但我们发现这么做不仅让我们的社区迅速壮大,也让我们发布新版本的速度变得更快了。最终带来了一个双赢的局面。
2.离开是不可避免的
调查发现贡献者离开的首要原因是他们没法挤出时间来参与贡献了,又或者他们当初贡献代码是因为工作上的需要,现在他们换了一份工作。总的来说贡献者的离去是在所难免的。
所以如果社区成员注定有一天要离开的话,拓展社区的唯一方式就是想办法留住新的贡献者。如果你不这么做,社区会随着旧成员的离去而逐渐地萎缩,无论你做什么都于事无补。
3.及时响应贡献者的反馈
人们(通常)不会介意对他们提交的代码进行再次的修改。甚至不介意修改多次。他们实际上介意的是当他们将代码提交上传三个月之后才得到评审的答复,告知他们需要对代码进行修改,然后还需要再等上三个月才被告知又要进行一次修改。延迟才是他们离开的最重要的原因,并非因为对于质量的苛求。
也有一些其他快速响应贡献者提交的代码的方式。举个例子,立即对提交代码的人表示感谢就是一个屡试不爽的办法,能大概率把新的贡献者“转化为”长期的开发者。
4.表现出极度的友善和不加掩饰的感激之情
对于每一个回复了我们调查的人,除去“我换工作了”和“我没有时间”外,其余离开的原因都是出乎意料的个人原因。
当人们在以志愿者的身份做出贡献时,他们并不奢求任何金钱上的回报,他们获得的是尊敬、赞许,以及将工作圆满完成的满足感,还有参与一个能够影响数百万人的产品所带来的成就感。所以只要有人贡献了一份自己的代码,你就应该对他们表示感谢。即使这份代码是完完全全需要被重写的垃圾,你依然要对他们表示感谢。因为他们对此已经投入了不少的汗水,如果你不这么做,在正式加入之前他们就已经想要离开了。
这里想表达的是,与指出人们错误相比,更重要的是对他们的贡献中积极的一面表达感谢和肯定。你必须真真切切地告诉贡献者你对他们的贡献表示感谢。你越是频繁和慷慨地做这件事,你留住贡献者的概率就越大。
5.避免对个体进行否定
要真心实意地,甚至近乎变态地和善,并且在这一点上千万不要吝惜。
移除障碍
下一个步骤就是要移除准入的门槛。究竟是什么阻碍着人们在贡献代码上迈出第一步呢?通常来说,最大的阻碍是缺乏文档和方向。当人们想要开始贡献代码时,他们想当然地会去思考应该如何贡献代码。
通过好几种方式来解决这个问题:
- 1.列出容易上手的项目
- 2.创建文档沟通的渠道
- 3.用优秀的、完整的以及简单的文档,描述一次代码提交应该是什么样的
- 4.让所有的文档更容易地被找到
让人们对项目感兴趣、用热门的编程语言编写项目、成为一个超级受欢迎的项目。
理解软件
第24章 什么是计算机
计算机是能够执行一系列符号指令,并且通过对数据进行比较以帮助人们达成目标的机器。
- 计算机能够对比数据。这有别于其他能够接受人类输入的机器。
- 计算机不仅能接受单条指令,还能接受一系列指令。比如一台简单的计算器只能处理一条指令,而计算机则强大得多,使它们区分开来。
- 和键盘上的一次按键一样,一次鼠标点击也可以算作“符号指令”。但是作为程序员,我们主要使用的符号指令是编程语言。所以作为程序员的我们在讨论该如何提升我们工作产出的质量时,更多的是在关心我们的程序的结构设计。
第25章 软件组件:结构、操作和结果
模型-视图-控制器(Model-View-Controller,MVC)模式之所以如此成功,是因为它反映了一个计算机程序最基础的本质:当一系列操作(action)施加于具有特定结构(structure)的数据之后,就会产生某种结果(results)。当然你的程序也需要接受各种输入,你可能会争辩需要把输入作为程序的第四个组成部分,但是我通常还是认为计算机由前三个部分组成:结构、操作和结果。
在MVC的语境里,模型就是结构,控制器替代了操作,视图则是结果。结构、操作和结果或许能够用于描述现存的所有机器。
一台机器可以拥有一些无法活动的部件,比如一个大型框架——这就是结构。一些可以被灵活控制并且参与实际工作的组成部分——这种动态的部分就是操作。最后机器会产出实体物品(否则它对我们就没有意义了)——这就是结果。
当我在编写软件时,我通常首先把结构搭建起来,然后编写操作部分,最后处理展示结果。有的人会从结果出发反向开始工作,这样也没有问题。但最不明智的选择或许是从操作部分开始入手,因为在既没有结构也没有结果的前提下执行的操作实在令人困惑。
第26章 重新审视软件:SAR/ISAR概念详解
任何计算机软件都由三个主要部分组成:结构、操作和结果。
一个程序或许还可以存在输入这类元素,它可以被认为是软件的第四个组成部分,尽管通常是用户而不是程序员创造了这一部分。所以我们既可以把这组概念缩写为SAR,也可以缩写为ISAR,这取决于我们是否想把“输入”这个概念也归纳进去。
SAR的应用场景比MVC要宽广得多,MVC是一种用于软件设计的模式,而SAR(或者ISAR)则是对于所有软件中三类(又或者四类)组成元素的描述。
SAR的迷人之处不仅在于它对整个程序适用,对程序的任意组成部分也同样适用。一个完整的程序拥有结构,但是单个函数或者是单行代码也同样拥有结构。对于操作和结果这两个概念也是如此。
在完整程序中能够被当作“结构”的一些例子:
- 代码的文件夹分布。
- 所有的类以及它们之间的关联方式。
- 如果你的程序需要用到数据库的话,数据库的结构(模式)也算是一种结构。(注意数据库中存储的数据并不能算作结构。如果你程序会生成数据并且将数据存储在数据库中,那么它们应该算作结果的一部分。如果数据已经存在而你的程序负责对它进行处理,数据则算作输入。)
- 一个独立的类(站在面向对象的角度上说)也拥有结构:
- 类里各种方法的名称,以及它们需要处理的参数的类型/名称。
- 类里变量的名称和类型(成员变量)。
无论一个函数(或者变量)是私有还是公有,它都算是结构的一部分,因为结构就是用于描述的这个东西是什么的(与之相反的是这个东西能做什么或者是能产出什么),而“私有”或者“公有”恰恰是用来描述这个东西是什么的词组。
结构是“程序的组件”或者是“程序的组成部分”。所以函数的名称和类型、变量的名称和类型,以及类——这些都是结构。
结构只是“摆在那里”。除非程序中的其他部分用到它,否则他不会给自己找事做。
操作,与一个完整程序有关的操作非常好理解。一个税务软件就是用来“处理税务”的,一个计算器程序就是“用来计算”的。操作一定是动词。“计算”“修复”“添加”“移除”,这些都属于操作。
在一个类里面,操作就是方法内的代码。你可以把它们当作各种各样不同类型的操作——有些事已经发生,有些事将要发生。在许多的编程语言中,你还可以在任意的类或者函数之外编写代码——那种启动程序时才运行的代码。它们也属于操作。
结果,每个程序,每个函数,每一行代码都会产生一些影响。它们会产生某种结果。
任何一种结果总是能用过去式来描述——它是某种已经被完成或者创建过的事物。
你程序中的代码片段也产出结果。当你调用一个方法或者函数时,会得到一个非常具体的结果。它会返回给你一些数据,或者它会造成一些数据的改变。无论程序(或者程序的某一部分)最终会产出什么,它们都算是结果。
第27章 软件即知识
软件从根本上来说是由知识组成的物体。它遵从所有与知识相关的规则和定律。它展现出的行为也和在任意场景下知识展现出的行为一模一样,除了不同软件体现的具体形式会有不同。
举个例子,当软件过于复杂时它很容易被误用。而当软件出错时(比如有了一个bug)它还有可能会给他人造成伤害以及引发问题。同样当人们对代码一知半解时,人们可能会无法对它们做出正确的修改。所有与知识有关的方方面面对软件也同样适用。错误的数据可能会导致人们犯错,错误的代码也会导致计算机犯错。
有人也希望软件,特别是代码,也表现得更有意义和更富有逻辑。因为代码就是知识,在人们阅读代码时,在脑海里它们应该能够立即被翻译成知识。如果代码做不到这一点,那就意味着代码其中的某部分过于复杂——或许是底层的编程语言或者是系统,但更有可能因为软件设计者创建的代码结构不够简约。
当我们在渴望知识时,可以通过不同的方式获取它。有人通过阅读获取知识,有人通过思考获取知识,有的人通过观察,有人通过实验,还有人通过交谈等。总的来说我们可以将这些方式划分为两类:是在自力更生获取信息(观察、实验、思考等);还是在借助他人获取知识(阅读、聊天等)。
当在判断某人解决问题时是需要编写新代码还是使用已有代码的时,这些原则也同样适用。你基本上不太可能包办从软件到硬件层面的所有代码,或者独立开发出当下十分受欢迎的软件。
当然有一些代码没有地方可复用,只有熟悉业务的我们才有资格编写——这部分代码通常是正在开发的产品的特殊业务逻辑部分。但是更多的时候我们还是要依赖现有的代码,就像作为人类个体我们必须依赖二手知识生存一样。
这些原则也可以用于在不同的开发者之中分配工作,是让人用第一手信息提前编写一部分代码会更快,还是让一群人同时对一个现有系统(二手知识)进行代码修改(对他们来说也算是第一手信息)会更快?答案很明显是依情况而定,尽管这里提出的观点并没有多新奇(有些程序员比其他人更了解系统,所以他们可以更快地完成),但是我们将结论推导出来的方法很重要。首先我们从理论上说明软件就是知识,然后我们发现了一条逻辑清晰的思路,它指向现存的一些普遍成立的原则。这意味着我们可以从这些已知的原则中找到其他更有用的信息。
第28章 技术的使命
时,最终的结果通常是成功的。而当尝试用它解决与人相关的比如思维、沟通、个人能力等问题的时候,它通常是失败的,甚至会事与愿违。
们可以和世界上任意一个人进行实时的交流。但是它不会让我们成为更好的沟通者。事实上它反而给许多非常差劲的沟通者提供了一个广阔的平台,让他们能够在上面传播仇恨和恐惧。
技术善于解决什么样的问题以及不善于解决什么样的问题:专注于用技术解决人类相关问题的公司更有可能失败。使用技术解决与实体物质相关问题的公司至少还有成功的概率。
看上去似乎存在一些关于这条规则的反例。举个例子,Facebook存在的意义不就是将人们连接在一起吗?这听上去是一个与人有关的问题,并且Facebook也做得非常成功啊。但是将人们连接起来并不是Facebook实际上在做的事情。它提供的只是一个供人们沟通的媒介而已,它并没有主动将人们联系起来。事实上,我认识的大多数人都对沉迷于Facebook感到反感——人们把时间都花费在了网络上,而不是对人类而言更为珍贵的线下生活中。
技术本身并没有好坏之分,但是当它在被尝试用于解决与人相关的问题时趋于变坏,而当它聚焦于解释现实世界物质有关的问题时趋于向好。
第29章 简单地聊聊互联网隐私
第一种类型的隐私是“空间隐私”。这类隐私权能够决定谁能或者不能进入一个特定物理空间,或许是因为你正处于那个空间,所以你并不希望某些特定的人进入这个空间。“进入空间”从定义上说也包括采取任何的方式方法来感知空间内发生的一切。这种形式的隐私是实实在在的。它的适用范围仅限于物理空间,从字面上理解就是说“我可以允许,也可以禁止你感知这个物理空间里发生的一切,我拥有掌控这件事的权力”。
我们之所以想要这种形式的隐私,最主要的原因是我们想要保护某人或者某物避免受到伤害,这里保护的对象通常是我们自己。这种形式的隐私和计算机程序无关,因为我们不认为与我们共处一室的计算机程序侵犯到了我们的隐私空间。我的文字处理软件不会侵犯我物理空间里的隐私,即使它与我“在同一个房间里”,因为它没有任何感知能力。唯一的例外是如果某个计算机程序将它接收到的一切(图像或者声音)传送到某个我们并不希望传送到的地方——这就算是侵犯隐私了,因为当我们不希望这一切有人知晓的时候,某人还是能够通过这个软件感知到空间里发生的一切。
第二种类型的隐私就是“信息隐私”。这种类型的隐私决定了某些人是否应该知晓某些事情。在计算机程序和互联网语境下,这才是我们通常讨论的隐私类型。独立的个体或者团体之所以希望信息隐私不受到侵犯,是因为他们相信隐私信息在落入他人之手之后,会增加给他们带来伤害的可能性。
无论你从事什么样的职业,为了生存,你必须和他人交换信息。你要做的事情越多,你需要交换的信息也就越多。
“每一条隐私信息在使用前都应该征求用户同意”的想法也是荒谬的。你希望你的浏览器在每一次你加载页面时,都询问你:“我可以向这个提供这个网页的网站发送你的IP地址吗?”如果你是一名驻扎在敌对国家的间谍,或许你希望这么做。但如果你是普通人,那可能只会给你带来烦躁——你不再会使用这款浏览器,并且转而搜寻其他可以替代的软件。但如果你真是一名间谍或者是反抗组织战士,你可能会使用洋葱路由器(Tor)来避免被追踪。
第30章 简约和安全
提升软件安全性的秘诀之一(也可能是最主要的因素)是保证软件足够简约。
当我们在考虑软件的安全性时,首先要问的问题是:“这个软件可能会遭受多少类攻击?”这等同于在问存在多少种“进入”软件的方式。更像是询问:“这幢建筑有多少扇门和窗?”如果这幢建筑只有一扇门与外界连通,看守它非常容易。但如果它有1000扇门,那么确保这幢建筑的安全似乎就不太现实了,无论每一扇门的安全性如何或者你雇佣了多少负责安全的警卫,都无法做到万无一失。
所以我们需要将“进入”软件的方式限制在指定的数量之下,否则安全无从谈起。通过让整套系统变得相对简约,又或者将它拆解为简约并且完全独立的组件,能够帮助我们达到提升安全性的目的。
一旦我们成功限制了进入软件的方式数量,接下来就要开始思考:
每一种进入软件的方式可能会被多少种攻击所利用?
我们可以通过尽可能简化“进入”方式本身来降低这些攻击的可能性。
如果这部分工作也完成了,然后我们就需要尽可能降低将攻击带来的最大损失。好比在一幢建筑中,我们要确保一扇门只会通往一个房间。
标准的UNIX系统只提供数量非常少的系统调用供绝大部分UNIX程序的实现使用。(即使扩展后系统调用总数也只有大概只有140种左右,而且其中的绝大部分在常见的程序中根本没有被使用过。)每一个系统调用所做的工作都极其具体,并且能力十分有限。
而Windows操作系统则有一堆十分荒唐并且让人疑惑的系统调用,每一个调用都需要传递太多的参数,所干的事情也过于繁杂。
如果你对系统提供的高级功能稍做了解的话,你会发现Windows提供的API算是庞大而复杂的。它们像是能够同时控制系统和界面的奇异野兽。而你在UNIX中找不到任何与之完全等价对应东西(因为在UNIX中系统和界面是完全分离的),但是我们还是可以将它们的部分组件进行比较。例如我们可以比较Windows提供的日志API和Linux下的日志API,但它们完全没有可比性,因为Windows下的日志API简直就是个笑话。对于Windows操作系统来说,任意一个组成部分都存在太多种类的“进入”方式,导致它从来没有安全可言。
获得安全保障的最佳方式是简单明了。
我们不应该在软件前布置千军万马来保障它的安全。而是应该借助限制软件只提供一些最基础的“入口”,来减少保护的需求,这些“入口”应该是直截了当和简单易懂的,并且还能免受被入侵的危害。
第31章 测试驱动开发和观察循环
每一个人都有关于如何编写代码各不相同的偏好,都有各自的道理。但是通过观察每一个人的偏好,你能够总结出一条通用的原则:“我需要对某件事物进行观察之后才能做出决定。”有些人在他们编写代码时需要观察相关测试的运行结果,有些人则需要通过观察他们正在编写的代码,才能决定接下来的代码要怎么写。甚至当他们在谈论到个人开发规则中的一些例外情况时,也总是会提到把留意到某件事作为他们开发过程的一部分。
这是可以一条可以应用在所有软件开发循环周期中的原则:
1 | 观察(Observation)→决策(Decision)→行动(Action)→观察 →决策→行动→…… |
如果你想给整个流程起一个名字,你可以称之为“观察循环”(Cycle of Observation)或者“ODA
每一个有效的开发流程都会将流程中的这类循环模式作为它主要的指导思想。甚至像敏捷开发这种大规模的涉及全团队的开发流程也是如此。事实上,敏捷开发只不过是那种已经被抛弃的,需要花费数月或者数年才能完成一个循环迭代开发模式(瀑布模型,也被称为“预先做大量设计(Big Design UpFront)”)的短周期版本(每几周)而已。
所以这么看来短周期似乎比长周期更好。大部分开发者的效率提升,都可以通过将ODA循环周期缩短为对开发者、团队或者是组织而言最小的合理时间来达成。
通常来说你可以通过将精力放在缩短观察时间上,来将整个循环周期时间缩短。一旦成功之后,周期的其他两部分就会自行加速(如果它们没有加速,
有三个主要因素会对观察阶段带来影响:
- 信息呈现给开发者的速度(例如能够快速给出反馈结果的测试)。
- 信息呈现给开发者的完整性(例如拥有完整的测试覆盖率)。
- 信息呈现给开发者的准确性(例如测试值得信赖)。
这能帮助我们理解近几十年来某些特定开发工具背后成功的成因。比如持续集成、线上环境监测系统、性能调试工具、代码调试工具、编译器中更明确的错误消息、能够突出显示错误代码的IDE——之所以所有这些工具能如此“成功”,是因为它们让观察这件事变得更快、更准确或者更完整了。
有一个问题需要注意——你必须确保你呈现信息的渠道,也是人们能从中获取到他们想要信息的渠道。如果你只是无脑地把大批量的信息倾倒给人们,而他们又无法轻易地从中找到他们关心的具体数据,那么这种数据可以说是无用的。好比如果没有人收到过一次线上环境的报警,那么这个报警是否存在也就不重要了。
如果一名开发者一直无法确认他接收到的信息的准确性,那么他很可能就会开始忽略这类信息。你必须确保成功地传递了信息,而不只是将它生产出来而已。
事实上还存在一类能够代表整个软件开发流程的“大ODA循环”——发现一个问题,确定解决方案,将它在软件中实现。在这个大循环中,还有许多小的循环(比如被分配到了一个功能需求,确定功能应该是如何工作的,然后将这个功能完成)。甚至在小循环中还存在更小的循环(观察到需求变更,确定如何实现,然后用代码编写),如此往复。
在所有这些可能的循环过程中,最棘手的往往是第一轮ODA循环,因为你需要在缺少前一轮决策或者行动的前提下做出观察。
第32章 测试的哲学
我们通过一种包含断言、观察和实验,并称之为“测试”的系统工具来获取与软件行为相关的知识。
从某种意义上说,软件测试是传统科学方法论的反向过程,传统的科学方法论是,你首先需要对宇宙进行验证,然后把实验得到的结果用于完善你的假设。
与之相反的在软件领域中,如果我们的“实验”(测试用例)不能证明我们的假设(测试做出的断言),那么则需要对正在测试的系统做出修改。
也就是说一旦某个测试失败了,很有可能是我们的软件需要修改,而不是我们的测试。当然有时候我们也需要对测试进行修改来确保它能够恰当地反映我们软件当前的状态。
通过对测试的价值、断言、边界、假设和设计进行检视,有助于对我们编写的测试进行重新思考。
测试的目的在于向我们传递系统的有关知识,这些知识其实存在不同层次的价值。
举个例子,不分场合地测试1+1是否依然等于2不会给我们带来任何有价值的知识。但是如果能让我意识到,即使我依赖调用的API做出了破坏性的修改,但我的代码依然能够正常工作,在这种情形下这部分信息还是能给我带来非常大的帮助的。
总的来说:
- 在创建一个有效和有用的测试之前,人们必须要清楚地知道自己想要获得什么样的信息。
- 只有恰当地对信息的价值作出判断,才能正确领悟应该把时间和精力投入哪些测试中。
如果说我们想要知道是什么让一个测试之所以能被称为测试,那么一定是因为它对某件事做出了断言,并且告知了我们断言的结果。人工测试人员可以对事物作出性质上的断言,比如某个颜色是否足够吸引人。但是自动化测试作出的断言必须是计算机有能力给出的,通常是判断一些可量化的具体陈述正确与否。
没有断言的测试不是一个测试。
我们会通过运行测试来熟悉我们的系统:断言结果的正确与否都能让我们学习到有关知识。
每一个测试都存在一定的边界,这是作为测试定义与生俱来的一部分存在的。
所以当在设计测试时,你应该知道什么需要被测试,什么不需要。
如果你编写了这样一个测试,很有可能你把多个测试合并成了一个,这些测试应该被分开。
每一个测试内都存在一组假设,这是测试在它的边界内能够高效执行的前提。
所有测试至少存在三种结果——通过、失败和未知。
结果为“未知”的测试不能说它们是失败的——否则就意味着他们向我们提供了某些关于系统的信息,但事实上它们没有。
所以我们需要对全套的测试进行设计,以便:
当我们将所有的测试组合在一起后,它们能够切实给予我们想要获取的所有知识。
“端到端”测试的意思是对一条完整的系统逻辑“路径”进行断言。也就是说你需要把整个系统搭建起来,在用户端执行一些操作,然后验证系统产出的结果。你并不关心系统内部为了达到这个目的是如何工作的,你只需要关心输入和结果。这基本上对所有测试都是成立的,但是在这里我们只在系统的最外层执行测试,也只检查最外层返回的结果。
端到端测试背后的主要思想是,通过我们尽可能以“真实”和“全面”的方式对系统进行测试,可以从断言中获取到极为精准的知识。路径上所有的交互和涉及的复杂逻辑都用测试进行覆盖。
只做端到端测试带来的问题是难以获取到关于系统的所有知识。在任何一个复杂的软件系统中,需要进行依赖和交互的组件数量和代码路径条数成爆炸级的增长,让测试很难或者不可能准确覆盖到所有的路径,并做出所有我们想实现的断言。
端到端测试还是有它的价值的,特别是对于完全缺少测试的系统来说是一个引入测试的很好切入点。它们也是当你想要检测整个系统组合起来是否能正常工作的有力工具。它们在测试套件中拥有重要的地位,但是就本身来说,它们并不是用于获取一个复杂系统全部知识的好的、长期的解决方案。
如果某个系统在经过设计之后只能以端到端的方式对其进行测试,那么这就是一个代码中存在架构问题的征兆。
这些问题应该通过重构来解决,目标是让系统也能够用上其他的测试方法。
集成测试下,你会取系统中的一个或者多个完整“组件”,用于专门测试将它们“组合在一起”后的表现行为如何。这里说的一个组件可以是一个代码模块、一个你系统依赖的库、一个提供数据的远程服务——本质上来说系统内任何一个从概念上可以和系统其他部分分离的内容都可以算作是一个组件。
与端到端测试相比,集成测试会将有待测试的组件独立出来,而不是把整个系统想象成一个“黑盒”对其进行测试。
集成测试不会遇到像端到端测试面临的那种糟糕的测试路径数量爆炸的问题,特别是当有待测试的组件本身和交互组件都非常简单的情况下。如果两个组件因为他们的交互极为复杂而导致难以进行集成测试,这或许在暗示我们其中的一个或者多个组件都需要被重构以便让组件变得更加简约。
就集成测试方法论本身来说它依然存在缺陷,如果想单纯地从组件间的交互来对整个系统进行分析的话,这意味着用于交互测试的组合数量必须要非常多,才能勾勒出整个系统行为的全景图。
与端到端测试相似,集成测试也存在可维护性方面的问题,尽管没有那么严重——当某人对其中一个组件的行为进行了更改,他可能需要更新所有与这个组件交互相关的测试。
单元测试,你需要单独选取一个组件,然后独立地对它的行为进行测试。
当你拥有一个组件,且这个组对于外部世界来说给出了极其慎重的承诺,那么单元测试则是用于验证这些承诺的最佳方式。
通常来说一个单元测试只会对一个类/模块中一个函数的单个行为进行验证。人们通常会为一个类/模块创建一组单元测试,当运行所有这些单元测试时,它们会覆盖你想验证的有关这个模块的所有行为。但这几乎总是意味着只测试系统的公共API,单元测试应该验证组件的行为,而不是实现。
理论上来说,如果系统中所有组件的行为在文档中都有完整的定义,且能按照文档里的行为挨个对每个组件进行测试的话,其实也是在对系统的所有可能行为进行测试。倘若你对其中一个组件的行为进行了更改,你只需更新围绕这个组件的最小测试集合即可。
很明显,只有当系统的组件在划分合理,以及简单到能够对行为做出完整定义的情况下,单元测试才能发挥出最大的功效。
现实世界里,在端到端测试和单元测试之间还存在不计其数的中间态测试类型。有时候你的测试方案介于单元测试和端到端测试之间。有时候你的测试又介于集成测试和端到端测试的交集当中。实际的系统会依赖所有形式的测试类型,用于帮助人们正确地理解系统行为。
举个例子,有时候你只需要对系统的其中一个部分进行测试,但是在内部实现上它依赖于系统的另一个部分,所以其实你也算是隐式地对那个系统进行了测试。但这并不意味着你当前的测试就是集成测试,它充其量只能算是间接对其他内部组件进行测试的单元测试而已——比一个普通的单元测试涉及面稍广一些,又比一个集成测试范围小一些。事实上这种类型的测试带来的效果通常是最好的。
通过“伪造数据”来对代码进行隔离在某些时候还是有用的。但人们必须要谨慎地作出决策,以及小心背后产生的成本,同时还需要通过对“伪”实例进行有意识的设计来缓解它们带来的副作用。值得注意的一点是,伪造数据还是能给我们的测试带来两方面的提升——确定性和速度。
如果系统或者它所处的环境中不存在任何变数,那么测试的结果也应该不会发生任何变化。
测试最有用的地方在于开发者们可以边编辑代码边运行它们,来检查他们正在编写的新代码是否能正常工作。如果测试运行变慢,那么这个功能也就逐渐变得没有意义。或者开发者们可以继续使用这些测试,但是编码的速度会被拖得越来越慢,因为他们不得不一直等待测试运行完毕。
一般来说,一个测试套件不应该花如此长的时间来运行,这会导致开发者在等待测试运行完毕的过程中,从工作上分心,以及无法集中注意力。现有研究表明对大部分开发者来说测试的理想运行时间应该在2到30秒之间。所以一个开发者在编辑代码阶段运行的测试套件应该尽量在这个时间区间内运行完毕。花上几分钟时间来运行测试没有问题,但是这不算是一个理想状态。更甚者如果要花上10分钟才能运行完毕,一般来说这是完全不可接受的。
缓慢的测试会对很多软件工程组织上的流程产生影响——降低这些影响最简单的办法就是让它们运行得足够快。
有一些工具能够在运行测试的情况下告诉你系统的哪些行代码被测试运行过了。它们将这个称为系统的“测试覆盖率”。这些工具有时候确实很有用,但是需要特别记住的是,它们其实并不会告诉你那些代码是否真的被测试过了,只是运行过而已。如果对代码行为没有执行过断言,那么它就算不上被测试过。
测试的总体目标是获取关于系统的有效知识。
这个目标凌驾于测试的其他一切原则之上——只要能带来这种效果,它就算是一个有效测试。
第七部分 持续改善
第33章 成功的秘密:持续改善
如果你想在软件方面获得成功,你所要做的仅仅是保证产品在每个版本中都能持续改善。
当一开始在决定选择使用什么软件的时候,人们的判断标准都各不相同。但是一旦人们做出了选择,他们就会一直使用下去直到一些原因迫使他们离开。只要在每一次发布中软件都得到持续改善,你就能挽留住你的绝大部分用户。
当然你发布新版本的频率必须足够频繁,才能让人们相信软件有希望持续好转。如果新版本总是难产,那么当前版本给用户带来的困扰只会止步不前。
如果你的软件项目想要获得成功,你所要做的仅仅是让它在每个版本中都到持续改善。
第34章 如何找到持续改善的空间
有时候软件项目中的重大问题难以得到解决,是因为它们需要投入大量的精力才能得以修复。但这并不意味着你可以忽略它们,而是要对项目做一个长远的修复规划,同时还要想办法如何保证版本迭代的稳定。
决定修复问题的优先级:对于Bugzilla项目来说,我们做了两件实实在在有助于我们决定优
先级的事情:
- 1.Bugzilla调查:https://wiki.mozilla.org/Bugzilla:Survey
- 2.Bugzilla可用性研究:https://wiki.mozilla.org/Bugzilla:CMU_HCI_Research_2008
这项调查中最重要的部分就是允许人们能以各种各样的文字形式,回答针对他们个人提出的问题。也就是说我个人会向Bugzilla的个体管理员发送问题,通常问题会针对他们的工作职责做一些定制化。这些问题中并不存在多选题,只会让他们告诉我什么正在困扰着他们以及他们想要看到什么功能。事实上他们非常乐意收到我的邮件——其中许多人对我做出的这次调查表达感谢。
一旦他们都回答完毕,我就会对所有回复一一过目,然后把提及的主要问题制作成一份列表——这简直是一份小小的惊喜!那么当下我们就把精力放在解决这些问题上,如果这些问题能得到改善,相信它们会让人们整体上对Bugzilla感到更满意。
而在可用性研究中,最能给我们带来帮助的环节,出乎意料的竟然是研究人员直接(他们通常是可用性的专家)坐在Bugzilla产品前,指出哪些功能违背了可用性的原则。也就是说,比他们做实际研究更有价值的是作为专家使用可用性工程的标准原则对产品的审视。他们作为从来没有使用过Bugzilla,也不会妥协说“好吧只能这么办”的小白用户,看待这个产品的新鲜视角很重要(至少我是这么想的)。
当你试图对事物进行改善时,首先需要解决的是当前已知的那些头部问题,无论它们的代价如何。然后情况会稍微缓和一些,可你依然会发现有一大堆问题需要解决。这时候你才需要从用户身上收集数据,修复他们认为的糟糕之处。
第35章 拒绝的力量
谁是这款软件的设计师,谁授权开发了这个功能?谁有权力阻止这个功能的上线,但是却袖手旁观任由灾难发生?
问题其实在于,如果你给了一群人允许他们把脑袋里的想法通通实现的自由,那么可以肯定他们每次实现的想法都是糟糕的。这不是对开发者的批评,而在真实生活中就是这样。我对开发者们的智力和能力有绝对的信心。我欣赏他们在软件开发过程中付出的努力和获得的成就。可不幸的事实是,在缺乏一些中心原则指导的情况下,人们会不自觉地让系统变得复杂起来,同时这也并不会给他们的用户带来任何帮助。
通常一名独立的软件设计师,还是有能力创建一款同时为用户和开发者带来一致愉悦体验的软件的。但如果独立设计师在其他开发者偏离产品目标的时候不及时站出来说“不”,那么系统很快就会崩塌,变成充斥着糟糕想法的大泥团。所以拥有一名有权力说“不”的软件设计师非常重要,在恰当的时候设计师能够准确地行使这份权力也很重要。
有非常多的软件设计原则能够告诉你糟糕的想法长什么样,同时它们还能引导你在十分必要的情况下对糟糕的想法说“不”。
- 如果功能的实现违反了软件设计中的某些原则(如过于复杂、难以维护、不易更改等),那么这类实现就是一个糟糕的想法。
- 如果功能不会给用户带来任何帮助,那么它就是一个糟糕的想法。
- 如果提议明显是愚蠢的,那么它就是一个糟糕的想法。
- 如果某些更改修复不了一个已知的问题,那么它就是一个糟糕的想法。
- 如果你不确定它是不是一个好的想法,那么它就是一个糟糕的想法。
有时设计师们会识别出一个糟糕的想法,但是因为他们当下想不到任何一个更好的解决方案,所以他们依然允许将它实现。这样的做法是错误的。如果对于某个问题你只能想到一个明显愚蠢的解决方案,那么你依然应该拒绝它。
问题在于:如果你真的将“糟糕的想法”实现了,那么你的“解决方案”会迅速变成比原问题带来更坏影响的灾难,它“能起作用”没错,但是接下来用户会开始抱怨,其他程序员会发出沮丧的感叹,系统也会崩溃,软件就变得不再那么受欢迎了。最终,“解决方案”变成了一个需要使用其他糟糕的“解决方案”来“修复”的问题。而这些“修复”本身也注定会演化为其他让人头疼的大问题。持续这样下去,终有一天你的系统会变得像当下许多现存软件系统一样臃肿、不易理解且难以维护。
理想情况下,当你拒绝了一个糟糕的想法,你应该提供一个额外的更好想法来替代它——这样才能使项目有建设性地向前推进,而不是让这个有待解决的问题成为开发过程中的一道障碍。但即使你当下想不到一个更好的想法,坚持拒绝糟糕的想法也很重要。好的想法总会出现。或许需要通过一些研究来发掘,或许某天你正在淋浴时忽然灵光一现,它就自然而然地出现了。我不知道想法会从哪里来以及它长什么样。但是不用担心。你要相信对于每一个问题总是存在解决它们的恰当方式。持续地寻找它们,不要放弃,不要向糟糕的想法妥协。
大部分时候,与其直接说“不”,不如说“哇,这个想法的这个部分听起来非常棒,但是其他部分有待商榷”。
我们应该把这个想法中闪光的那一部分提取出来,在经过加工打磨之后将它们利用起来。你必须对想法里糟糕的部分说不。想法中存在优秀的部分并不意味着整个想法都是优秀的。汲取想法里的精华,提炼它,围绕它拓展出一些更好的想法,直到你最终设计的解决方案无懈可击。
第36章 为什么说程序员糟糕透了
之所以计算机的使用体验异常糟糕,是因为程序员编写了一大堆疯狂、复杂、没有人能理解的玩意,并且复杂性还在不断往上叠加,直到程序的方方面面都陷入难以维系的地步。
绝大部分(90%或者更多)的程序员对于他们正在做的事情完全没有概念。
相当数量的程序员从一开始就不知道他们自己究竟在做什么。他们只是在模仿其他程序员犯下的错误——复制代码,然后往机器里输入一些指令,期待着它能如我们期望的那样工作。所有这些操作的背后都缺乏对计算机运作原理、软件设计原则,或每一个他们往计算机中输入字符的理解。
许多程序员根本就不知道在软件开发中可能存在通用法则或者是通用指南,所以他们根本就不会搜寻它们。许多软件公司也不会设法提升开发者对于他们所使用的编程语言的理解——也许因为他们仅仅认为程序员“如果被雇用了就应该对那些内容了如指掌”。
在哪些方面需要投入更多时间来学习:
- 你清楚地了解你编写的每一页代码上的每一个单词和符号吗?
- 你是否阅读过以及是否能完全理解与你使用的每一个函数有关的说明文档?
- 你是否掌握了软件开发中基本原则的精髓——掌握的程度足以让你毫无差池地解释给你团队中的新成员听?
- 你是否理解计算机内每一个组件的功能,以及它们是如何协同运作的?
- 你是否了解计算机的历史,以及它们未来的发展方向,以便帮助你理解你的代码将会如何运作在未来的计算机中?
- 你是否了解编程语言的历史,以便你可以了解你正在使用的编程语言将会如何进化,以及为什么会朝这个方向进化?
- 你是否了解其他的编程语言,其他的编程方式,以及其他形式的计算机,帮助你对症下药解决实际工作问题?
如果你只是简单地复制别人的代码,然后祈祷它也能在你这正常工作,那么你永远也不会成为一名优秀的程序员。
第37章 快速编程的秘诀:停止思考
给开发者施加时间上的压力的确会导致他们写出复杂的代码。但是交付的最后期限和复杂性并没有必然关系。
与其说“最后期限迫使我无法写出简单代码”,不如说“我写出简单代码的速度不够快”。进一步说,作为程序员你编码的速度越快,代码质量被最后期限影响的可能性就越低。
任何时候只要你发现自己停止了思考,那就意味着某个地方出了问题。
“需要打太多的字”对于开发者来说从来就不是一个会对效率产生影响的问题。恰恰相反,正是你停止输入的间隙拖慢了编码速度。
开发者之所以停下来思考,通常是因为他们没有完全理解一些单词或者符号的意义。
如果你发现自己正停下来思考,不要尝试去解决当下你脑海中的问题——而是跳出你自己,想想有什么是你自己还不知道的。然后去阅读那些能够帮助你理解它们的材料。
这种模式甚至能够解答类似于“用户是否真的会阅读这些文字?”这类的问题。也许公司内部没有用户体验研究部门能帮助你真正回答它,但是你至少可以画出一个原型,然后将它展示给人们并且征询他们的意见。不要只坐在那里然后纯粹地想这个问题——行动起来。只有行动才能带来理解。
很多人停下来思考,是因为他们没法在脑袋里一次性装下所有的概念——许多事物都在以一种复杂的方式相互关联在一起,人们必须首先在脑海中将它们过一遍才行。在这种情况下,更有效的方式往往是将它们写下来或者是画出来,而不是凭空思考它们。
有时候的问题是:“我不知道首先应该编写哪一部分的代码。”最简单的解决办法是,开始编写你现在有能力编写的任何代码。挑选当前问题中你已经完全理解的那一部分,首先编写该部分的解决方案——哪怕只有一个函数,或者只是一个并不重要的类。
通常情况下,一开始最容易编写的代码往往是应用程序中的“核心”部分。如果你依然不确定如何编写核心代码,那么可以从你已经确定的部分开始。
我发现只要问题的其中一个部分得到了解决,剩下的部分也会变得容易起来。有时候问题可以在逐步拆解的过程中慢慢清晰起来。只要你首先解决了一部分,剩余部分的解法自然也就一目了然了。哪一部分代码不需要太多思考就能开始编写,那么现在编写这部分代码就好。
另一个会产生理解方面问题的时刻是当你跳过了正常开发过程中的一些步骤的时候。在开发系统的过程中,不要跳过某些步骤还期望自己的效率很高。
第38章 开发者的傲慢
我唯一关心的是你的程序是否能够协助我完成工作。仅此一点决定了你的软件出色与否,如果它能达成这个目标,那么你应该对此感到骄傲。不要用那些仅仅是你认为重要的功能来博取我的眼球。
还有一些看似微不足道的地方也会带来问题,它们都和吸引了用户太多的注意力有关:
- “用户对于在使用我的产品之前,需要填写三个屏幕长的表单这件事,是不会有意见的。”
- “我非常肯定用户有兴趣了解我专程为这个程序发明的这些图标,所以把这些图标上的文字移除掉不成问题!”
- “我十分肯定借由这些弹出框来中断用户工作的做法是正确的。”
- “用户肯定想要在这个巨幅页面上搜索出某一小段文字,以便他们能够点击它。”
- “为什么我们要把这个变得更简单呢?要花费不少时间,对我来说……它已经够简单了。”
对于一个程序员来说,真正的谦逊是自愿地抹去他在用户世界里的存在感。
请不要再告诉用户你的程序已经安装在他的电脑上了。不要认为用户会在意你的程序,他们只是想要花时间用一用它的界面,或者想要学习它如何使用,他们在意的不是你的程序——而是他们想要达成的目标。如果你能帮助用户完美地实现他们的目标,那么就意味着你为他们创造出了最完美的程序。
第39章 “一致”并不意味着“统一”
在用户界面中,相似的事物应该看起来是相同的。而不同种类的事物应该看起来是不同的。
在应用的后端开发中,所有代码都应该在统一的技术框架之上进行开发。但那并不意味着界面上所有元素都应该看起来一样。
虽然说一致性在应用的前端和后端都非常重要,但是那并不意味着每一件事物都应该看上去是一样的。
第40章 用户有困难,开发者有方案
在软件的世界里,软件开发者的职责就是为用户解决问题。不同的用户代表着不同的问题,开发者们需要将它们一一解决。如果他们的角色发生了互换,麻烦可能就会接踵而至。
一旦你解决的是开发者的问题而不是用户的问题,那就意味着你投入精力的方向有误,这条路并非帮助人们解决问题的最佳方案。
我还感受到的一点是,解决开发者的问题比解决普通用户的问题更加复杂。所以找到用户实际存在的痛点并且解决它,会相比苦思冥想解决一个想象中的问题更容易。
只有用户(更准确地说,是大部分的用户,或者关于大部分用户的数据)才能真正告诉你他们遭遇的问题是什么,只有在开发侧(更准确地说,是在完全理解了这个问题,并且很可能听取了来自他人的反馈之后,被授予决策重任)的人才能对应该实施什么样的解决方案做出正确决定。
第41章 即时满足=即时失败
软件从来就是一个长期的过程。
一旦竞争对手X公司发布了“闪亮新功能”,我公司就会毫不示弱:“我们现在也必须要有闪亮新功能!”。这不是一个长期的制胜策略,而是短视下的恐慌的表现。你的用户不会因为其他某款软件有而你没有的功能,就立即起身转而投奔他们。你应该观察用户量的增长或者流失趋势再做决策,而不是无脑地对当下环境做出立即响应。
所以什么是好的长期策略?重构你的代码,让你能够在将来更轻松地添加功能就是其中之一。或者在产品发售之后对一些功能和UI上的不足进行打磨,也是用户喜闻乐见的。另一条建议是,如果某些功能可有可无,并且你将来也并不想维护它们,那还是不要添加了。
太多所谓“如何经营你的软件生意”方面的建议,它们关注的都是实时满足——你当下能做什么。新增功能!立即从投资人那里获取数百万美元的投资!但不幸的是,世界的运转法则其实是,毁灭事物可以是一瞬间的事,但是创造事物则需要花费时间。
所以在现实世界中,你越是向往“实时满足”,你也越是在将你的产品、生意和你的未来推向毁灭。
所以软件行业里一则关键经验教训是:如果你正在制订一则计划,请确保它至少承认创造这件事是需要耗费时间的。它可能不会是永远,但一定不会是当下立即实现。
第42章 成功来自执行而非创新
我的想法是如何得优秀或者多么有创新精神一点都不重要,重要的是我能让它在真实世界落实得多好。
所以请不要焦虑于“赶紧想出一个新点子”。这种担心没有必要。你只需要以尽可能完美的方式去实现一个现有的想法就好。你可以加上一些自己的创意,或者再进行一些打磨,但你根本不需要全新的东西。
第43章 杰出的软件
一款真正被称为杰出的程序,需要能够准确执行用户的意图。
如果你想要把这句称述做更详细的拆解,也就是说杰出软件必须做到:
- 1.完全按照用户的要求去做。
- 2.表现的行为和用户期望的完全一致。
- 3.不会妨碍用户传达他们的意图。
当计算机能完美地执行你给出的指令,会给人带来一种奇怪的满足感。这也是编程的乐趣之一——一旦计算机准确无误地将你下达的指令执行完毕,满足的愉悦感便油然而生。
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com